Stand: 10. Oktober 2018

Technologie und Terminologie

  • Die im PolMine-Projekt aufbereiteten Korpora werden aus Ausgangsformaten (pdf, plain text, html) in standardisierte XML-Formate übersetzt. Die Standardisierung erfolgt entspricht Vorgaben der Text Encoding Initiative (TEI).

  • Das TEI-XML des GermaParl-Korpus kann als Beispiel dienen. Es ist über ein GitHub-Repositorium offen zugänglich. Es ist, sinnvoll, sich dieses Ausgangsformat anzusehen!

  • Das XML-TEI ist geeignet für die dauerhafte Datenhaltung und zur Sicherung von Interoperabilität, nicht jedoch für eine effiziente Analyse. Als “indexing and query engine” nutzt das PolMine Projekt (das polmineR-Paket) die Corpus Workbench (CWB)

  • CWB-indizierte Korpora können insbesondere auch linguistische Annotationen speichern und für die Analyse verfügbar machen. Diese werden über “positionale Attribute” (p-attributes) verfügbar.

  • Metadaten sind in der Terminologie der CWB als strukturelle Attribute (s-attributes) verfügbar. Wichtig: S-Attribute sind nicht auf die Textebene beschränkt, sondern können auch Passagen von Text (z.B. Annotationen, Named Entities, in Parlamentsprotokollen: Zwischenrufe) unterhalb der Textebene auszeichnen.

Erforderliche Installationen und Initialisierung

Der Foliensatz nutzt das polmineR-Paket und das GermaParl-Korpus. Die Installation wurde im vorhergehenden Foliensatz ausführlicher erläutert.

Bitte beachten Sie, dass die Funktionalität für den folgenden Workflow erst mit polmineR-Version 0.7.10 zur Verfügung steht. Installieren Sie bei Bedarf die aktuelle Version das polmineR-Pakets.

Für die folgenden Beispiel laden wir zunächst polmineR. Außerdem wird für die Beispiele das data.table-Paket benötigt.

library(polmineR)
library(data.table)

Anzeige der vefügbaren Korpora

Die corpus()-Methode (ohne Argumente) gibt eine Liste der Korpora an, auf die in Analysen zugegriffen werden kann. Die Tabelle gibt in der zweiten Spalte die Korpusgröße an. Der Spalte “template” gibt Auskunft, ob Regeln zur Formatierung bei Volltextanzeigen verfügbar sind.

corpus()
##          corpus   size template
## 1 GERMAPARLMINI 222201     TRUE
## 2       REUTERS   4050     TRUE

In ihrer grundlegenden Verwendungsweise kann die size()-Methode genutzt werden, um die Größe eines Korpus direkt abzufragen.

size("REUTERS")
## [1] 4050

Die Korpora, die Sie hier sehen, sind als Beispieldaten im polmineR-Paket enthalten.

Aktivieren von Kopora in Datenpakten mit use

Um die Korpora zu aktivieren, die in einem R-package residieren, nutzt man die Funktion use().

use("GermaParl")

Indem wir nun noch einmal corpus() aufrufen, können wir prüfen, dass nun auch das GERMAPARL-Korpus verfügbar ist.

corpus()
##          corpus      size template
## 1     GERMAPARL 101013708     TRUE
## 2 GERMAPARLMINI    222201     TRUE
## 3       REUTERS      4050     TRUE

Beachte: Entsprechend den Konventionen der CWB werden Korpora immer in Großbuchschraben geschrieben.

Nutzung von Korpora an anderen Speicherorten

Die CWB benötigt für den Zugriff auf Korpora eine Beschreibung der Korpora, die über txt-Dateien einem sogenannten ‘registry’-Verzeichnis erfolgt. Tatsächlich wird immer beim Laden von polmineR ein temporäres registry-Verzeichnis angelegt, zu dem nach der Aktivierung von Korpora in Paketen weitere registry-Dateien hinzugefügt werden. Den Pfad zu diesem Verzeichnis können Sie mit der registry()-Funktion abfragen.

registry()

Die klassische Arbeitsweise mit der CWB sieht vor, dass man ein Standard-registry-Verzeichnis hat, das über die Umgebungsvariable CORPUS_REGISTRY definiert wird. Beim Start prüft polmineR, ob diese Umgebungsvariable definiert ist. Wenn ja, werden die registry-Dateien in diesem Verzeichnis in das oben beschriebene temporäre Verzeichnis kopiert. Die CORPUS_REGISTRY-Umgebungsvariable können Sie wie folgt definieren. Wichtig: Dies muss erfolgen, bevor Sie polmineR laden.

Sys.setenv(CORPUS_REGISTRY = "/PFAD/ZU/REGISTRY/VERZEICHNIS")

Tip: Am leichtesten ist es, Umgebungsvariablen für R über die Datei .Renviron zu definieren, die von R immer beim Start evaluiert wird. Durch Aufruf der Hilfe zu den Routinen beim Start von R erfahren Sie mehr (?Startup).

Liniguistische Annotationen: Positionale Attribute

  • Korpora werden in die CWB in tokenisierter Form importiert (Tokenisierung = Zergliederung des ursprünglichen Fließtextes in Worte / “Token”).

  • Jedem Token des Korpus wird bei der Indizierung ein eindeutiger numerischer Wert zugewiesen (“corpus position”, Abkürzung “cpos”).

  • Ergänzend zu der ursprüngliche Wortform im Ursprungstext, wird bei linguistisch annotierten Korpora (im Regelfall) eine Wortarterkennung (“part-of-speech”-Annotation, kurz “pos”) und eine Lemmatisierung der Token (Rückführung des Worts auf Grundform ohne Flektion, “lemma”) durchgeführt.

  • Mit der p_attributes()-Methode frägt man die p-Attribute eines Korpus ab.

p_attributes("GERMAPARL")
## [1] "lemma" "pos"   "word"

Die Tabelle auf der folgenden Seite vermittelt die Datenstruktur mit positionalen Attributen (p-attributes) und Korpus-Positionen (cpos). Der Text kann von oben nach unten gelesen werden.

CWB-Datenstruktur: Tokenstream

cpos word pos lemma
0 Liebe NN lieb
1 Kolleginnen NN Kollegin
2 und KON und
3 Kollegen NN Kollege
4 , $, ,
5 die ART d
6 Sitzung NN Sitzung
7 ist VAFIN sein
8 eröffnet VVPP eröffnen
9 . $. .

Grundsätzlich ist diese Datenstruktur vergleichbar mit jener, die Sie vielleicht auch vom tidytext-package kennen.

Strukturelle Attribute (‘s-attributes’ )

Metadaten eines Korpus werden als strukturelle Attribute (s-attributes) bezeichnet. Welche s-Attribute bei einem Korpus verfügbar sind, fragen Sie mit der Methode s_attributes() ab.

s_attributes("GERMAPARL")
##  [1] "party"               "parliamentary_group" "speaker"            
##  [4] "lp"                  "session"             "date"               
##  [7] "role"                "interjection"        "agenda_item"        
## [10] "agenda_item_type"    "src"                 "url"                
## [13] "year"

Die Dokumentation eines Korpus sollte erklären, was die s-Attribute bedeuten. Um zu ermitteln, welche Ausprägungen es für ein s-Attribute gibt, nutzen Sie das Argument s_attribute.

s_attributes("GERMAPARL", s_attribute = "year")
##  [1] "1996" "1997" "1998" "1999" "2000" "2001" "2002" "2003" "2004" "2005"
## [11] "2006" "2007" "2008" "2009" "2010" "2011" "2012" "2013" "2014" "2015"
## [21] "2016"

Korpusgröße

Open wurde schon erwähnt, dass Sie mit der size()-Methode die Größe eines Korpus abfragen können.

size("GERMAPARL")
## [1] 101013708

Wenn Sie zusätzlich mit dem Argument s_attribute ein S-Attribut angeben, schlüsseln Sie die Korpusgröße entsprechend auf.

size("GERMAPARL", s_attribute = "lp")
##    lp     size
## 1: 13 11676618
## 2: 14 19349263
## 3: 15 12785509
## 4: 16 18412812
## 5: 17 23418060
## 6: 18 15371446

Rezept: Balkendiagramm mit Korpusumfang

In einem kleinen Beispiel wollen wir mit einem Balkendiagramm visualisieren, wie die Zahl der Worte in den Plenarprotokollen variiert. Zunächst ermitteln wir mit der s_attributes()-Methode die Größe des Korpus differenziert nach Jahren.

s <- size("GERMAPARL", s_attribute = "year")

Dann machen wir daraus ein Balkendiagramm, wobei wir auf der Y-Achse die Größe des Korpus in Tausend Token angeben.

barplot(
  height = s$size / 1000,
  names.arg = s$year,
  main = "Größe GermaParl nach Jahr",
  ylab = "Token (in Tausend)", xlab = "Jahr",
  las = 2
  )

Die damit erzeugt Graphik kommt auf der folgenden Folie.

In den Jahren 1998, 2002, 2005, 2009 und 2013 sehen wir jeweils geringere Korpusumfänge. Welchen systematischen Grund hat das?

Korpusgröße: Zwei S-Attribute

Bei der size()-Methode kann auch ein zweites S-Attribut angegeben wird, dann wird eine Tabelle mit Korpusgrößen differenziert nach den beiden Merkmalen ausgegeben.

Beachte: Der Rückgabewert ist hier ein data.table, nicht ein data.frame, das Standard-Datenformat von R für Tabellen. Viele Operationen können mit data.tables weitaus schneller als mit data.frames durchgeführt werden. Daher nutzt das polmineR-Paket intern intensiv data.tables. Ein Umwandlung in data.frames erfolgt nicht, ist aber problemlos möglich.

dt <- size("GERMAPARL", s_attribute = c("speaker", "party"))
df <- as.data.frame(dt) # Umwandlung in data.frame
df_min <- subset(df, speaker != "") # In wenigen Fällen wurde Sprecher nicht erkannt
head(df_min)
##                 speaker party   size
## 6        Achim Großmann   SPD 113226
## 7            Achim Post   SPD   4785
## 8  Adelheid D. Tröscher   SPD  29462
## 9        Adolf Ostertag   SPD  44002
## 10           Adolf Roth   CDU  24900
## 11         Agnes Alpers LINKE  18824

Redeanteile

Korpusgröße: Zwei Dimensionen

In einem zweiten Beispiel zur Arbeit mit den Ergebnissen einer Untergliederung des Korpus nach zwei Kriterien stellen wir die Frage, wie die Redeanteile der Fraktionen zwischen den Legislaturperioden geschwankt hat.

dt <- size("GERMAPARL", s_attribute = c("parliamentary_group", "lp"))
dt_min <- subset(dt, parliamentary_group != "") # Bearbeitung data.table wie data.frame

Die Tabelle, die wir jetzt haben, ist in einer sogenannten “extensiven” Form. Sie kann folgendermaßen in eine Normalform gebracht werden.

tab <- dcast(parliamentary_group ~ lp, data = dt_min, value.var = "size")
setnames(tab, old = "parliamentary_group", new = "Fraktion")

Das schauen wir uns an, wobei wir ein ‘widget’ benutzen, das mit der JavaScript-Bibliothek DataTable (nicht verwechseln mit data.table!) erzeugt wird. (Die Ausgabe lässt sich auch in Folien einbeziehen, die - wie diese - mit Rmarkdown geschrieben wurden.)

DT::datatable(tab)

Wortzahl nach Fraktion und Jahr

Vorbereitungen für den barplot

Für den gruppierten barplot brauchen wir eine Matrix, welche die Höhe der Balken angibt.

pg <- tab[["Fraktion"]] # Für Beschriftung des barplot "retten" wir die Fraktionen
tab[["Fraktion"]] <- NULL # Spalte "Fraktionen" wird an dieser Stelle beseitigt
m <- as.matrix(tab) # Umwandlung des data.table in Matrix
m[is.na(m)] <- 0 # Wo NA-Werte in der Tabelle sind, ist die Korpusgröße 0

Der letzte “Dreh” ist ein Vektor mit den Farben, die den Fraktionen üblicherweise zugeordnet sind. Dieser ist benannt, so dass über eine Indizierung die Zuweisung der Farben erfolgen kann, ohne dass man versehentlich verrutschen könnte.

colors <- c(
  "CDU/CSU" = "black", FDP = "yellow",
  SPD = "red", GRUENE = "green", LINKE = "pink", PDS = "pink",
  fraktionslos = "lightgrey", parteilos = "darkgrey"
  )

Let’s go

Den barplot auszugeben, ist nun keine Zauberei mehr.

barplot(
  m / 1000, # Höhe der Balken - Zahl Worte, in Tausend
  xlab = "Worte (in Tausend)", # Beschriftung der X-Achse
  beside = TRUE, # Gruppierung
  col = colors[pg] # Farben der Balken, Indizierung gewährleistet richtige Reihenfolge
  )
# Um die Legende zweispaltig gestalten zu können, erstellen wir die Legende gesondert.
legend(
  x = "top", # Platzierung Legende oben mittig
  legend = pg, # Beschriftung mit Benennung Fraktion
  fill = colors[pg], # Indizierung gewährleistet, dass nichts verrutschen kann
  ncol = 2, # zweispaltige Legende
  cex = 0.7 # kleine Schrift
  )

Korpus nach Legislaturperiode und Fraktion

Kenne deine Daten!

Das Beispiel einer Visualisierung der Korpusgröße nach Fraktionszugehörigkeit und Legislaturperiode ist nicht ganz zufällig gewählt. In der 15. Wahlperiode gibt es einen gar nicht so kleinen Redeanteil von Sprechern, die “fraktionslos” sind. Wenn Sie die gleiche Analyse auf Ebene von Parteizugehörigkeit durchführen: Was sehen Sie da? Die fraktionslosen Abgeordneten der 15. Wahlperiode sind Angehörige der PDS.

Dies ist keine Einführung in das GermaParl-Korpus, aber der richtige Ort für den Hinweis, dass jede gute Analyse ein gutes Verständnis der Daten zur Voraussetzung hat.

Lesen Sie die Dokumentation der Daten und sehen Sie sich die Daten an, in diesem Fall das TEI-XML. Was für jede andere Datenart eine Selbstverständlichkeit ist, gilt auf für Korpora: Wenn man zu wenig über die Daten weiß, ist die Wahrscheinlichkeit schlechter Forschung groß.

Diskussion und Ausblick

Zunächst eine Ermutigung: Der Einstieg in die Arbeit mit data.tables erfordert Umdenken, lohnt sich aber, nicht nur wegen der Effizienz dieser Datenstruktur. Als Beispiel dient das folgende “snippet”.

size("GERMAPARL", s_attribute = "speaker")[speaker == "Angela Merkel"]
##          speaker   size
## 1: Angela Merkel 701455

Nochmal das Stichwort “know your data”: Wenn Sie eine Blick in das TEI-XML des GermaParl-Korpus geworfen haben: Zwischenrufe sind - bewusst! - Teil der “XMLifizierung” der Protokolle, in der sie ausgezeichnet sind. Für saubere Analysen muss man also mit Subkorpora arbeiten, die Zwischenrufe aus der Analyse ausschließen. Wie das geht, ist Gegenstand des nächsten Foliensatzes.